﻿using System;
using System.Concurrency;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceModel;
using Collab.Modules.Editing;
using Collab.Modules.Editing.Interfaces;
using Collab.Services.Remote;
using Collab.Services.Remote.Contracts;
using Collab.Services.Remote.Entities;
using System.Collections.Generic;

namespace Collab.Modules.Session
{
    public class CollaborationHostSession : CollaborationSession
    {

        private readonly ServiceHost serviceHost;
        private readonly IRemoteCollaborationHost service;
        private Solution solutionStructure;

        public CollaborationHostSession(string name, DocumentTracker tracker, StateTracker stateTracker, SolutionBuilder solutionBuilder)
            : base(name, tracker, stateTracker, solutionBuilder)
        {

            this.disposable.Add(this.documentTracker.WhenViewActivated.ObserveOnDispatcher().Subscribe(OnViewActivated));
            this.disposable.Add(this.documentTracker.WhenViewClosed.ObserveOnDispatcher().Subscribe(OnViewClosed));



            this.service = new RemoteCollaborationService(this.user, this.CreateSynchronization, this.GetSolutionInformation);
            this.serviceHost = new ServiceHost(this.service);
            this.serviceHost.Description.Endpoints.Clear();
            System.ServiceModel.Description.ServiceEndpoint endpoint = serviceHost.AddServiceEndpoint(typeof(IRemoteCollaborationService),
                                                                           CollaborationBinding.CreateCollaborationBinding(),
                                                                           "net.tcp://localhost:8888");

            this.serviceHost.Faulted += (sender, e) => Debug.WriteLine("ServiceHost has faulted. Restart the service.");
            this.serviceHost.Open();

            this.disposable.Add(this.solutionBuilder.WhenOpenedSolution.Where(x => x != null).Subscribe(x => this.service.OpenedSolution(x)));

            this.disposable.Add(this.documentTracker.WhenSelectedViewChanged.ObserveOnDispatcher().Subscribe(OnViewChanged));

            SetupCommonServiceOperations(this.service, this.service);

            this.disposable.Add(this.service.WhenFileAdded.Subscribe(fileInformation => this.solutionBuilder
                .AddFile(fileInformation, (fileRequests, Id, readyCallback) => DownloadFiles(fileRequests, Id).Subscribe(x => { }, () =>
                {
                    readyCallback();
                    this.service.PublishFileAdded(fileInformation);
                }))));

            

            //TODO: Should this really exist? Doesn't work properly
            this.documentTracker.ActiveViews.Run(docView =>
                {
                    IEditBuffer editBuffer = new VSXEditBuffer(docView.View.TextBuffer, this.user.Id, docView.Name);
                    editBuffer.Subscribe(SendChange);
                    this.editBuffers[docView.Name] = editBuffer;
                });


            this.SessionState = Session.SessionState.Ready;
        }



        private DocumentSnapshot CreateSynchronization(string documentName)
        {
            Debug.WriteLine("Recived request for:" + documentName);
            if (!this.editBuffers.ContainsKey(documentName))
            {
                Debug.WriteLine("The key did not exist");
                var editBuffer = new VirtualEditBuffer(this.user.Id, this.solutionBuilder.SolutionPath, documentName);
                this.editBuffers[documentName] = editBuffer;
                editBuffer.Subscribe(SendChange);
            }
            Debug.WriteLine("Returning" + this.editBuffers[documentName].DocumentName);
            return this.editBuffers[documentName].CurrentSnapshot;
        }

        private Solution GetSolutionInformation()
        {
            Debug.WriteLine("Get solution information");
            this.solutionStructure = this.solutionBuilder.ParseSolution();
            return this.solutionStructure;
        }

        private void OnViewActivated(DocumentView docView)
        {
            this.user.Status = new UserStatus(this.user.Id, docView.Name);
            this.service.PublishUserStatus(this.user.Status);
            if (this.editBuffers.ContainsKey(docView.Name)) // Use existing virtual edit buffer
            {
                var virtualBuffer = this.editBuffers[docView.Name];

                //TODO: Not sure this does what I think it does
                var updateReplay = this.service.WhenChangeReceived
                    .Where(update => update.DocumentName == docView.Name) // only need updates for the specific document
                    .Replay(); // start buffering missed updates

                this.editBuffers[docView.Name] = null; // stop receiving updates

                var editBuffer = new VSXEditBuffer(docView.View.TextBuffer, virtualBuffer.UserId, virtualBuffer.DocumentName, virtualBuffer.CurrentSnapshot);
                editBuffer.Subscribe(SendChange);

                this.editBuffers[docView.Name] = editBuffer; // start receiving updates

                updateReplay.Subscribe(editBuffer).Dispose(); // replay missed updates

                virtualBuffer.Dispose();
            }
            else
            {
                var editBuffer = new VSXEditBuffer(docView.View.TextBuffer, this.user.Id, docView.Name);
                editBuffer.Subscribe(SendChange);
                this.editBuffers[docView.Name] = editBuffer;
            }
        }

        private void OnViewChanged(DocumentView docView)
        {
            if (docView == null)
                return;

            this.service.PublishUserStatus(new UserStatus(this.user.Id, docView.Name));
        }

        private void OnViewClosed(DocumentView docView)
        {
            Debug.Assert(this.editBuffers.ContainsKey(docView.Name));
            var vsxBuffer = this.editBuffers[docView.Name];

            var virtualBuffer = new VirtualEditBuffer(vsxBuffer.UserId, this.solutionBuilder.SolutionPath, vsxBuffer.DocumentName, vsxBuffer.CurrentSnapshot);// Convert into virtual buffer
            this.editBuffers[docView.Name] = virtualBuffer;
            virtualBuffer.Subscribe(SendChange);

            vsxBuffer.Dispose();
        }

        private void SendChange(BufferUpdate update)
        {
            this.service.PublishChange(update);
        }

        private IObservable<Unit> DownloadFiles(IDictionary<string, string> fileRequests, Guid userID)
        {
            return Observable.CreateWithDisposable<Unit>(o => Scheduler.NewThread.Schedule(() =>
            {
                var files = fileRequests.Select(x =>
                this.service.FileChunkReceived
                    .Where(chunk => chunk.FileName == x.Key)
                    .TakeWhile(chunk => !chunk.EOF)
                    .Aggregate(new List<FileChunk>(), (chunks, chunk) =>
                    {
                        chunk.FileName = x.Value;
                        chunks.Add(chunk);
                        return chunks;
                    }))
                .Merge()
                .Subscribe(chunks =>
                {
                    using (var fileStream = new FileStream(chunks.First().FileName, FileMode.Create))
                    {
                        chunks.Run(x => fileStream.Write(x.Data, 0, x.Data.Length));
                    }
                }, () =>
                {
                    o.OnNext(new Unit());
                    o.OnCompleted();
                });

                fileRequests.Run(request =>
                    this.service.PublishFileRequested(new FileRequest { FileName = request.Key, Sender = this.user.Id }, userID));
            }));
        }

        public override void Dispose()
        {
            this.SessionState = Session.SessionState.Disconnecting;
            this.serviceHost.Close();
            base.Dispose();
            this.serviceHost.Faulted -= (sender, e) => Debug.WriteLine("ServiceHost has faulted. Restart the service.");
        }
    }
}
